#  config.rb
#
#  This file is part of myvnet.
#
#  Copyright (C) 2008  Filipe David Borba Manana,  fdmanana@gmail.com
#
#  myvnet is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  myvnet is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with myvnet.  If not, see <http://www.gnu.org/licenses/>.


require 'singleton'
require 'getoptlong'
require 'logger'
require 'yaml'
require 'lib/protocols/ipv4'
require 'lib/exceptions'


# Module for myvnet configuration.
# 
# Creates the following global variables:
#
# $log::
#   Hash with symbols as keys, representing log names, and instances of Logger 
#   class as values. The names of logs are found in the constants 
#   Config::FILE_LOG_NAMES and Config::CON_LOG_NAMES.
#
# $settings::
#   Instance of class Config::Settings. Contains the settings for myvnet after 
#   the command line options and config file have been parsed.   
#
module Config
  
  VERSION = '0.0.2'
  DEFAULT_CONFIG_FILE = 'config.yml'
  MAX_LOG_SIZE = 1_048_576
  MAX_LOG_COPIES = 10
  
  # Name of all the logs whose destinations are files.
  #
  FILE_LOG_NAMES = [:main, :packet]
  
  # Name of all the logs whose destinations are by default
  # the console - standard output and standard error.
  #
  CON_LOG_NAMES = [:stdout, :stderr]
  
  
  class Settings
    include Singleton
    
    attr_accessor :config_file
    attr_accessor :logs_dir
    attr_accessor :interface
    attr_accessor :net_addresses
    attr_accessor :debug    
    
    alias iface interface
    alias net_addrs net_addresses
    alias iface= interface=
    alias net_addrs= net_addresses=
  end
  
  
  # custom Logger::Formatter for console messages
  class ConsoleFormatter < Logger::Formatter
    CFFORMAT = "[%5s] %s"
    
    def call(severity, *args)
      msg = args[2] + "\n"
      CFFORMAT % [severity, msg2str(msg)]
    end
  end
  
  # custom Logger::Formatter for messages destined to files
  class LogFileFormatter < Logger::Formatter
    LFFORMAT = "[%5s, %s] %s"
    
    def initialize
      super
      @datetime_format = "%d-%b-%Y %H:%M:%S"
    end
    
    def call(severity, time, *args)
      msg = args[1] + "\n"
      LFFORMAT % [severity, format_datetime(time), msg2str(msg)]
    end
  end
  
  
  def self.init
    if ARGV.include? '--version' or ARGV.include? '-v'
      show_version
    end

    $settings = Settings.instance
    $settings.config_file = DEFAULT_CONFIG_FILE
    $settings.logs_dir = './logs'
    $settings.debug = false
    $settings.interface = 'lo'
    $settings.net_addresses = ['127.0.0.1']

    if ARGV.include? '--help' or ARGV.include? '-h'
      usage
    end
            
    # create basic loggers
    $log = {} # to easily access all Logger objects
    
    # standard output
    log = Logger.new $stdout
    log.formatter = ConsoleFormatter.new
    log.level = Logger::INFO
    $log[:stdout] = log
    
    # standard error
    log = Logger.new $stderr
    log.formatter = ConsoleFormatter.new
    log.level = Logger::WARN
    $log[:stderr] = log
    #
    
    # Check first for config file. Command line options override config file options.
    if ARGV.include? '--config-file' or ARGV.include? '-f'
      i = ARGV.index '--config-file'
      i = ARGV.index '-f' if i.nil?
      
      if ARGV[i+1] == nil
        $log[:stderr].fatal "Option `#{ARGV[i]}' requires an argument"
        exit 1
      end
      
      filename = ARGV[i+1].strip
      if filename.empty?
        $log[:stderr].fatal "Invalid file name"
        exit 1
      end
      
      $settings.config_file = filename
      ARGV[i,2] = nil
    end
             
    load_config_file $settings.config_file
    #
        
    opts = GetoptLong.new(
                          ['--debug', GetoptLong::NO_ARGUMENT],
                          ['--logs-dir', GetoptLong::REQUIRED_ARGUMENT],
                          ['--net-addresses', GetoptLong::REQUIRED_ARGUMENT],
                          ['--interface', '-i', GetoptLong::REQUIRED_ARGUMENT]
    )    
    opts.quiet = true
    
    begin      
      opts.each do |opt, arg|
        case opt
          when '--debug'
            $settings.debug = true
          when '--logs-dir'
            $settings.logs_dir = arg
          when '--net-addresses'
            $settings.net_addresses = parse_net_addresses(arg)
          when '--interface'
            $settings.interface = arg
        end
      end
    rescue GetoptLong::Error
      $log[:stderr].fatal "#{opts.error_message}"
      exit 1
    end
    
    init_loggers $settings.logs_dir
    
    $log[:main].info "myvnet successfully initialized."
    
    if $log[:main].debug?
      $log[:main].debug("Settings are:\n" + $settings.to_yaml)
    end
    
    $settings
  end
  
  
  # private module functions:
  
  def self.usage(quit = true, retval = 0)
    puts <<_EOH_    
Usage: myvnetd [OPTION]*

  Launches the myvnet daemon.

Options:

  -h, --help                                Displays this help and exits.
  
      --debug                               Enable debug mode.
      
  -f, --config-file filename                Configuration file to load.
                                            Defaults to `#{$settings.config_file}'.
                                              
      --logs-dir dirname                    Directory where myvnet logs are stored.
                                            Defaults to `#{$settings.logs_dir}'.
      
  -i, --interface iface                     Name of the network interface card to use
                                            for sending and sniffing packets.
                                            Defaults to `#{$settings.interface}'.

      --net-addresses address[,address]*    Comma separated list of network addresses 
                                            for the simulated hosts. Defaults to the
                                            single element list `#{$settings.net_addresses[0]}'.

  -v, --version                             Displays myvnetd version and exits.

_EOH_
    
    exit retval if quit
  end  
  
  
  def self.show_version(quit = true, retval = 0)
    puts "myvnetd version #{VERSION}\n\n"
    exit retval if quit
  end  
  
  
  def self.parse_net_addresses(list)
    list = list.split(/ \s* , \s* /x)
    # empty strings when consecutive commas found
    list.delete_if { |a| a.empty? }
    
    list.each do |a|
      begin
        IPv4.to_bstream_addr a
      rescue BadArgFormat
        $log[:stderr].fatal "Invalid IPv4 address : `#{a}'"
        exit 1
      end
    end
    
    list    
  end
  
  
  def self.init_loggers(logs_dir)
    begin
      Dir.mkdir logs_dir
    rescue Errno::EEXIST
      # directory already exists, do nothing
    rescue Exception => ex
      $log[:stderr].fatal "Could not create logging directory `#{logs_dir}' : #{ex.message}"
      exit 1
    end
    
    FILE_LOG_NAMES.each do |logname|
      filename = logs_dir + '/' + logname.to_s + '.log'
      
      begin
        file = File.new(filename, "a")
      rescue Exception => ex
        $log[:stderr].fatal "Could not create file `#{filename}' : #{ex.message}"
        exit 1
      end
      
      file.sync = true  # skip Ruby's buffering      
      log = Logger.new(file, MAX_LOG_COPIES, MAX_LOG_SIZE)
      log.formatter = LogFileFormatter.new
      log.level = Logger::INFO
      
      $log[logname] = log
    end
    
    if $settings.debug
      $log.each_value do |log|
        log.level = Logger::DEBUG
      end
    end
  end
  
  
  def self.load_config_file(filename)
    
    begin
      h = YAML.load_file filename
    rescue Errno::ENOENT
      # file not found
      unless filename == DEFAULT_CONFIG_FILE
        msg = "Config file `#{filename}' was not found."
        $log[:stderr].warn msg
      end
      return
    rescue Exception => ex
      # file exists, but some error occurred when loading it
      msg = "Could not load config file `#{filename}' : #{ex.message}"
      $log[:stderr].warn msg
      return
    end
    
    unless h.is_a? Hash
      msg = "Config file `#{filename}' ignored. It does not have a hash format."
      $log[:stderr].warn msg
      return      
    end
    
    # validate options' values
    case h['debug']
      when TrueClass
      when FalseClass
      when NilClass
      else
        $log[:stderr].warn "Invalid value for `debug' option in config file `#{filename}'."
        h['debug'] = nil
    end
    
    case h['logs_dir']
      when String
      when Numeric
        h['logs_dir'] = h['logs_dir'].to_s
      when NilClass
      else
        $log[:stderr].warn "Invalid value for `logs_dir' option in config file `#{filename}'."
        h['logs_dir'] = nil
    end
    
    case h['interface']
      when String
      when Numeric
        h['interface'] = h['interface'].to_s
      when NilClass
      else
        $log[:stderr].warn "Invalid value for `interface' option in config file `#{filename}'."
        h['interface'] = nil
    end
    
    case h['net_addresses']
      when Array
        parse_net_addresses h['net_addresses'].join(',')
      when String
        h['net_addresses'] = parse_net_addresses(h['net_addresses'])
      else
        $log[:stderr].warn("Invalid value for `net_addresses' option in config file `#{filename}'." +
                            " It must be either an array or a comma separated list of addresses.\n")
        h['net_addresses'] = nil
    end
    #
    
    $settings.debug = h['debug'] || $settings.debug
    $settings.logs_dir = h['logs_dir'] || $settings.logs_dir
    $settings.interface = h['interface'] || $settings.interface
    $settings.net_addresses = h['net_addresses'] || $settings.net_addresses
    
  end
  
  
  private_class_method :usage
  private_class_method :show_version
  private_class_method :parse_net_addresses
  private_class_method :init_loggers
  private_class_method :load_config_file
  
end
